package com.bodgittandscarper.broker.server;

import static java.util.logging.Level.INFO;

import java.io.EOFException;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Logger;

import com.bodgittandscarper.broker.types.DatabaseField;
import com.bodgittandscarper.broker.types.DatabaseSchema;
import com.bodgittandscarper.broker.types.DatabaseSchemaMod;
import com.bodgittandscarper.broker.types.Record;
import com.bodgittandscarper.broker.types.RecordMod;
import com.bodgittandscarper.broker.types.TypeFactory;

/**
 * 
 * Main Database handler. When the Database is initialized, the specified database file is read 
 * and the content is stored in a List. 
 * 
 * @author Morten Rummelhoff
 *
 */
public class Database {

	private Logger log = Logger.getLogger(getClass().getName());

	private int magicNumber;

	private int sizeOfOneRecord;
	private long startOfRecords;
	
	private List<RecordMod> recordList = new ArrayList<RecordMod>();

	private DatabaseSchemaMod databaseSchema;

	private RandomAccessFile raf;
	
	/**
	 * Constructor that initializes the database from a specified database file. The database file is
	 * read and all {@link Record} are stored in a {@link List}. <br>
	 * A {@link RandomAccessFile} is used to access the database. 
	 * 
	 * @param databaseFile String representation of the database file.
	 * @throws FileNotFoundException If specified databaseFile cannot be found.
	 * @throws IOException If the database file is corrupt.
	 */
	public Database(String databaseFile) throws FileNotFoundException, IOException {
		//use a RandomAccessFile for accessing the database file
		raf = new RandomAccessFile(databaseFile, "rwd");

			//Start of file
			magicNumber = raf.readInt();
			if (log.isLoggable(INFO))
				log.log(INFO, "Magic Number: " + magicNumber);

			boolean moreBytesToRead = true;

			sizeOfOneRecord = raf.readInt();
			if (log.isLoggable(INFO))
				log.log(INFO, "Length of each record: " + sizeOfOneRecord);

			int numberOfFields = raf.readShort();

			//Database Schema
			databaseSchema = TypeFactory.getInstance().createDatabaseSchemeMod();

			for (int i = 0; i < numberOfFields; i++) {

				int fieldnameLengths = raf.readShort();
				byte[] fieldName = new byte[fieldnameLengths];
				raf.readFully(fieldName);

				short fieldnameValueLengths = raf.readShort();

				DatabaseField dbField = TypeFactory.getInstance().
				createDatabaseField(new String(fieldName, "US-ASCII"), fieldnameValueLengths);

				databaseSchema.addDatabaseField(dbField);

			}

			//save filemarker here. End of DatabaseScheme
			startOfRecords = raf.getFilePointer();

			//Data Section
			int recordNumber = 0;
			
			//Try read until end of file.
			while (moreBytesToRead) {
				try {

					byte validFlag = raf.readByte();	


					String[] fields = new String[numberOfFields];
					for (int i = 0; i < numberOfFields; i++) {

						DatabaseField databaseField = databaseSchema.getDatabaseField(i);						

						byte[] recordField = new byte[databaseField.getFieldSize()];
						raf.readFully(recordField);

						fields[i] = new String(recordField, "US-ASCII");
					}

					RecordMod record = TypeFactory.getInstance().createRecordMod(recordNumber++, (validFlag == 0), false,  fields);
					if (log.isLoggable(INFO))
						log.log(INFO, "Record read: " + record);

					recordList.add(record);


				} catch (EOFException e) {
					moreBytesToRead = false;
				}

			}
			if (log.isLoggable(INFO))
				log.log(INFO, "Number of Records: " + recordList.size());

	}
	
	/**
	 * Checks if a given Record number exist in the database. This method does not check whether or not 
	 * the Record's valid flag is set. 
	 * 
	 * @param recNo Record number to check.
	 * @return true if a {@link Record} exist with the specified recNo. Else false
	 */
	public boolean doesRecodExist(int recNo) {
		return (recNo >= 0 && recNo < recordList.size()) ;
	}

	/**
	 * This method makes sure that each String object in the String array are adjusted to fulfill
	 * the Database Schema according to field length and format. 
	 * 
	 * The correction is made the the <tt>copyOf</tt> method defined in the Arrays class.
	 * 
	 * @param data Array of Record field data that shall be adjusted to fit into the Database Schema.
	 * @see Arrays#copyOf(byte[], int)
	 */
	public void adjustRecordFieldsToComplyDBStructure(String[] data) {

		//adjust data for database structure 
		for (int i = 0; i < databaseSchema.getDatabaseFieldCount(); i++) {
			DatabaseField dbField = databaseSchema.getDatabaseField(i); 

			//correct bytesize to match fieldvaluelength
			try {
				data[i] = new String(Arrays.copyOf(data[i].getBytes("US-ASCII"), dbField.getFieldSize()), "US-ASCII");
			} catch (UnsupportedEncodingException e) {
				//User cannot put unsupported charset into record. Reset field(fill with spaces(32 decimal)) and print stacktrack
				Arrays.fill(data[i].getBytes(), (byte)32);
				e.printStackTrace();
			}	
		}
	}

	
	/**
	 * Gets the total number of Records. Records not valid are also included.
	 * 
	 * @return total number of Records in the Database.
	 */
	public int getNumberOfRecords() {
		return recordList.size();
	}
	
	/**
	 * Gets the specified {@link Record} from the Database.
	 * 
	 * @param recNo The {@link Record} to fetch.
	 * @return the {@link RecordMod}
	 */
	public RecordMod getRecord(int recNo) {			
		return recordList.get(recNo);
	}

	/**
	 * Adds a {@link Record} to the Database internal list. 
	 * 
	 * @param record The {@link Record} to add.
	 */
	public void addRecord(RecordMod record) {
		recordList.add(record);
	}
	
	/**
	 * Writes a Record object to the database file. It uses the seek
	 * method from the {@link RandomAccessFile} to find the exact place to write the record bytes.
	 * 
	 * @param record The Record to persist.
	 */
	public void persistRecord(Record record) {
		//adjust Record to comply with databaseschema (structure)
		adjustRecordFieldsToComplyDBStructure(record.getRecordFields());
		
		try {

			int recordNumber = record.getRecordNumber();

			//find where to write in databasefile
			raf.seek(startOfRecords + (recordNumber * (sizeOfOneRecord+1)));

			if (log.isLoggable(INFO))
				log.log(INFO, "Placed filepointer at: " + raf.getFilePointer() + ", Ready to write: " + record);

			//valid byte
			raf.write(record.isValid() ? new byte[] {0} : new byte[] {1});

			for (int i = 0; i < databaseSchema.getDatabaseFieldCount(); i++) {

				//fields has been verified to respect database structure
				raf.write(record.getRecordFields()[i].getBytes("US-ASCII"));

			}
			//for each record, make sure to sync to disk.
			raf.getFD().sync();

		} catch (IOException ioe) {
			ioe.printStackTrace();			
		}

		
	}
	
	/**
	 * Gets a DatabaseSchema for the Database. The DatabaseSchema is initialized when the Database file is initialized. 
	 * This is done in the constructor. 
	 * 
	 * @return DatabaseSchema for Database. 
	 */
	public DatabaseSchema getDatabaseSchema() {
		return databaseSchema;
	}
	
	
	
}
